Re-do Ifbb0f6751 in a smaller scope as a first step.
Change-Id: I346f3587d3bfeaf0fe3467cd1f4dcf2d134ecc08
"maintenance/jsduck/external.js",
"resources/src/mediawiki",
"resources/src/mediawiki.action",
- "resources/src/mediawiki.api",
"resources/src/mediawiki.language",
"resources/src/mediawiki.messagePoster",
"resources/src/mediawiki.page",
'position' => 'top',
),
'mediawiki.api' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.js',
+ 'scripts' => 'resources/src/mediawiki/api.js',
'dependencies' => array(
'mediawiki.util',
'user.tokens',
'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api.category' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.category.js',
+ 'scripts' => 'resources/src/mediawiki/api/category.js',
'dependencies' => array(
'mediawiki.api',
'mediawiki.Title',
),
),
'mediawiki.api.edit' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.edit.js',
+ 'scripts' => 'resources/src/mediawiki/api/edit.js',
'dependencies' => array(
'mediawiki.api',
'mediawiki.Title',
'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api.login' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.login.js',
+ 'scripts' => 'resources/src/mediawiki/api/login.js',
'dependencies' => 'mediawiki.api',
),
'mediawiki.api.options' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.options.js',
+ 'scripts' => 'resources/src/mediawiki/api/options.js',
'dependencies' => 'mediawiki.api',
'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api.parse' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.parse.js',
+ 'scripts' => 'resources/src/mediawiki/api/parse.js',
'dependencies' => 'mediawiki.api',
'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api.upload' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.upload.js',
+ 'scripts' => 'resources/src/mediawiki/api/upload.js',
'dependencies' => array(
'dom-level2-shim',
'mediawiki.api',
),
),
'mediawiki.api.watch' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.watch.js',
+ 'scripts' => 'resources/src/mediawiki/api/watch.js',
'dependencies' => array(
'mediawiki.api',
),
'dependencies' => 'mediawiki.ForeignApi.core',
),
'mediawiki.ForeignApi.core' => array(
- 'scripts' => 'resources/src/mediawiki.api/mediawiki.ForeignApi.js',
+ 'scripts' => 'resources/src/mediawiki/ForeignApi.js',
'dependencies' => array(
'mediawiki.api',
'oojs',
+++ /dev/null
-( function ( mw, $ ) {
-
- /**
- * Create an object like mw.Api, but automatically handling everything required to communicate
- * with another MediaWiki wiki via cross-origin requests (CORS).
- *
- * The foreign wiki must be configured to accept requests from the current wiki. See
- * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> for details.
- *
- * var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' );
- * api.get( {
- * action: 'query',
- * meta: 'userinfo'
- * } ).done( function ( data ) {
- * console.log( data );
- * } );
- *
- * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter
- * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this
- * doesn't guarantee that it's the same user.)
- *
- * Authentication-related MediaWiki extensions may extend this class to ensure that the user
- * authenticated on the current wiki will be automatically authenticated on the foreign one. These
- * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See
- * CentralAuth for a practical example. The general pattern to extend and override the name is:
- *
- * function MyForeignApi() {};
- * OO.inheritClass( MyForeignApi, mw.ForeignApi );
- * mw.ForeignApi = MyForeignApi;
- *
- * @class mw.ForeignApi
- * @extends mw.Api
- * @since 1.26
- *
- * @constructor
- * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint.
- * @param {Object} [options] See mw.Api.
- *
- * @author Bartosz DziewoĆski
- * @author Jon Robson
- */
- function CoreForeignApi( url, options ) {
- if ( !url || $.isPlainObject( url ) ) {
- throw new Error( 'mw.ForeignApi() requires a `url` parameter' );
- }
-
- this.apiUrl = String( url );
-
- options = $.extend( /*deep=*/ true,
- {
- ajax: {
- url: this.apiUrl,
- xhrFields: {
- withCredentials: true
- }
- },
- parameters: {
- // Add 'origin' query parameter to all requests.
- origin: this.getOrigin()
- }
- },
- options
- );
-
- // Call parent constructor
- CoreForeignApi.parent.call( this, options );
- }
-
- OO.inheritClass( CoreForeignApi, mw.Api );
-
- /**
- * Return the origin to use for API requests, in the required format (protocol, host and port, if
- * any).
- *
- * @protected
- * @return {string}
- */
- CoreForeignApi.prototype.getOrigin = function () {
- var origin = location.protocol + '//' + location.hostname;
- if ( location.port ) {
- origin += ':' + location.port;
- }
- return origin;
- };
-
- /**
- * @inheritdoc
- */
- CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) {
- var url, origin, newAjaxOptions;
-
- // 'origin' query parameter must be part of the request URI, and not just POST request body
- if ( ajaxOptions.type === 'POST' ) {
- url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url;
- origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin;
- url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) +
- 'origin=' + encodeURIComponent( origin );
- newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } );
- } else {
- newAjaxOptions = ajaxOptions;
- }
-
- return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions );
- };
-
- // Expose
- mw.ForeignApi = CoreForeignApi;
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.category
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Determine if a category exists.
- *
- * @param {mw.Title|string} title
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {boolean} return.done.isCategory Whether the category exists.
- */
- isCategory: function ( title ) {
- var apiPromise = this.get( {
- prop: 'categoryinfo',
- titles: String( title )
- } );
-
- return apiPromise
- .then( function ( data ) {
- var exists = false;
- if ( data.query && data.query.pages ) {
- $.each( data.query.pages, function ( id, page ) {
- if ( page.categoryinfo ) {
- exists = true;
- }
- } );
- }
- return exists;
- } )
- .promise( { abort: apiPromise.abort } );
- },
-
- /**
- * Get a list of categories that match a certain prefix.
- *
- * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
- *
- * @param {string} prefix Prefix to match.
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string[]} return.done.categories Matched categories
- */
- getCategoriesByPrefix: function ( prefix ) {
- // Fetch with allpages to only get categories that have a corresponding description page.
- var apiPromise = this.get( {
- list: 'allpages',
- apprefix: prefix,
- apnamespace: mw.config.get( 'wgNamespaceIds' ).category
- } );
-
- return apiPromise
- .then( function ( data ) {
- var texts = [];
- if ( data.query && data.query.allpages ) {
- $.each( data.query.allpages, function ( i, category ) {
- texts.push( new mw.Title( category.title ).getMainText() );
- } );
- }
- return texts;
- } )
- .promise( { abort: apiPromise.abort } );
- },
-
- /**
- * Get the categories that a particular page on the wiki belongs to.
- *
- * @param {mw.Title|string} title
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
- * if title was not found.
- */
- getCategories: function ( title ) {
- var apiPromise = this.get( {
- prop: 'categories',
- titles: String( title )
- } );
-
- return apiPromise
- .then( function ( data ) {
- var titles = false;
- if ( data.query && data.query.pages ) {
- $.each( data.query.pages, function ( id, page ) {
- if ( page.categories ) {
- if ( titles === false ) {
- titles = [];
- }
- $.each( page.categories, function ( i, cat ) {
- titles.push( new mw.Title( cat.title ) );
- } );
- }
- } );
- }
- return titles;
- } )
- .promise( { abort: apiPromise.abort } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.category
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.edit
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
-
- /**
- * Post to API with edit token. If we have no token, get one and try to post.
- * If we have a cached token try using that, and if it fails, blank out the
- * cached token and start over.
- *
- * @param {Object} params API parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} See #post
- */
- postWithEditToken: function ( params, ajaxOptions ) {
- return this.postWithToken( 'edit', params, ajaxOptions );
- },
-
- /**
- * API helper to grab an edit token.
- *
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string} return.done.token Received token.
- */
- getEditToken: function () {
- return this.getToken( 'edit' );
- },
-
- /**
- * Post a new section to the page.
- *
- * @see #postWithEditToken
- * @param {mw.Title|String} title Target page
- * @param {string} header
- * @param {string} message wikitext message
- * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
- * @return {jQuery.Promise}
- */
- newSection: function ( title, header, message, additionalParams ) {
- return this.postWithEditToken( $.extend( {
- action: 'edit',
- section: 'new',
- title: String( title ),
- summary: header,
- text: message
- }, additionalParams ) );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.edit
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
-
- /**
- * @class mw.Api
- */
-
- /**
- * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
- * `options` to mw.Api constructor.
- * @property {Object} defaultOptions.parameters Default query parameters for API requests.
- * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
- * @private
- */
- var defaultOptions = {
- parameters: {
- action: 'query',
- format: 'json'
- },
- ajax: {
- url: mw.util.wikiScript( 'api' ),
- timeout: 30 * 1000, // 30 seconds
- dataType: 'json'
- }
- },
-
- // Keyed by ajax url and symbolic name for the individual request
- promises = {};
-
- // Pre-populate with fake ajax promises to save http requests for tokens
- // we already have on the page via the user.tokens module (bug 34733).
- promises[ defaultOptions.ajax.url ] = {};
- $.each( mw.user.tokens.get(), function ( key, value ) {
- // This requires #getToken to use the same key as user.tokens.
- // Format: token-type + "Token" (eg. editToken, patrolToken, watchToken).
- promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
- .resolve( value )
- .promise( { abort: function () {} } );
- } );
-
- /**
- * Constructor to create an object to interact with the API of a particular MediaWiki server.
- * mw.Api objects represent the API of a particular MediaWiki server.
- *
- * var api = new mw.Api();
- * api.get( {
- * action: 'query',
- * meta: 'userinfo'
- * } ).done( function ( data ) {
- * console.log( data );
- * } );
- *
- * Since MW 1.25, multiple values for a parameter can be specified using an array:
- *
- * var api = new mw.Api();
- * api.get( {
- * action: 'query',
- * meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
- * } ).done( function ( data ) {
- * console.log( data );
- * } );
- *
- * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
- * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
- *
- * @constructor
- * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
- * each individual request by passing them to #get or #post (or directly #ajax) later on.
- */
- mw.Api = function ( options ) {
- // TODO: Share API objects with exact same config.
- options = options || {};
-
- // Force a string if we got a mw.Uri object
- if ( options.ajax && options.ajax.url !== undefined ) {
- options.ajax.url = String( options.ajax.url );
- }
-
- options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
- options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
-
- this.defaults = options;
- };
-
- mw.Api.prototype = {
-
- /**
- * Perform API get request
- *
- * @param {Object} parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise}
- */
- get: function ( parameters, ajaxOptions ) {
- ajaxOptions = ajaxOptions || {};
- ajaxOptions.type = 'GET';
- return this.ajax( parameters, ajaxOptions );
- },
-
- /**
- * Perform API post request
- *
- * TODO: Post actions for non-local hostnames will need proxy.
- *
- * @param {Object} parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise}
- */
- post: function ( parameters, ajaxOptions ) {
- ajaxOptions = ajaxOptions || {};
- ajaxOptions.type = 'POST';
- return this.ajax( parameters, ajaxOptions );
- },
-
- /**
- * Massage parameters from the nice format we accept into a format suitable for the API.
- *
- * @private
- * @param {Object} parameters (modified in-place)
- */
- preprocessParameters: function ( parameters ) {
- var key;
- // Handle common MediaWiki API idioms for passing parameters
- for ( key in parameters ) {
- // Multiple values are pipe-separated
- if ( $.isArray( parameters[ key ] ) ) {
- parameters[ key ] = parameters[ key ].join( '|' );
- }
- // Boolean values are only false when not given at all
- if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
- delete parameters[ key ];
- }
- }
- },
-
- /**
- * Perform the API call.
- *
- * @param {Object} parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} Done: API response data and the jqXHR object.
- * Fail: Error code
- */
- ajax: function ( parameters, ajaxOptions ) {
- var token,
- apiDeferred = $.Deferred(),
- xhr, key, formData;
-
- parameters = $.extend( {}, this.defaults.parameters, parameters );
- ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
-
- // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
- if ( parameters.token ) {
- token = parameters.token;
- delete parameters.token;
- }
-
- this.preprocessParameters( parameters );
-
- // If multipart/form-data has been requested and emulation is possible, emulate it
- if (
- ajaxOptions.type === 'POST' &&
- window.FormData &&
- ajaxOptions.contentType === 'multipart/form-data'
- ) {
-
- formData = new FormData();
-
- for ( key in parameters ) {
- formData.append( key, parameters[ key ] );
- }
- // If we extracted a token parameter, add it back in.
- if ( token ) {
- formData.append( 'token', token );
- }
-
- ajaxOptions.data = formData;
-
- // Prevent jQuery from mangling our FormData object
- ajaxOptions.processData = false;
- // Prevent jQuery from overriding the Content-Type header
- ajaxOptions.contentType = false;
- } else {
- // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug
- // So let's escape them here. See bug #28235
- // This works because jQuery accepts data as a query string or as an Object
- ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' );
-
- // If we extracted a token parameter, add it back in.
- if ( token ) {
- ajaxOptions.data += '&token=' + encodeURIComponent( token );
- }
-
- if ( ajaxOptions.contentType === 'multipart/form-data' ) {
- // We were asked to emulate but can't, so drop the Content-Type header, otherwise
- // it'll be wrong and the server will fail to decode the POST body
- delete ajaxOptions.contentType;
- }
- }
-
- // Make the AJAX request
- xhr = $.ajax( ajaxOptions )
- // If AJAX fails, reject API call with error code 'http'
- // and details in second argument.
- .fail( function ( xhr, textStatus, exception ) {
- apiDeferred.reject( 'http', {
- xhr: xhr,
- textStatus: textStatus,
- exception: exception
- } );
- } )
- // AJAX success just means "200 OK" response, also check API error codes
- .done( function ( result, textStatus, jqXHR ) {
- if ( result === undefined || result === null || result === '' ) {
- apiDeferred.reject( 'ok-but-empty',
- 'OK response but empty result (check HTTP headers?)'
- );
- } else if ( result.error ) {
- var code = result.error.code === undefined ? 'unknown' : result.error.code;
- apiDeferred.reject( code, result );
- } else {
- apiDeferred.resolve( result, jqXHR );
- }
- } );
-
- // Return the Promise
- return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
- if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
- mw.log( 'mw.Api error: ', code, details );
- }
- } );
- },
-
- /**
- * Post to API with specified type of token. If we have no token, get one and try to post.
- * If we have a cached token try using that, and if it fails, blank out the
- * cached token and start over. For example to change an user option you could do:
- *
- * new mw.Api().postWithToken( 'options', {
- * action: 'options',
- * optionname: 'gender',
- * optionvalue: 'female'
- * } );
- *
- * @param {string} tokenType The name of the token, like options or edit.
- * @param {Object} params API parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} See #post
- * @since 1.22
- */
- postWithToken: function ( tokenType, params, ajaxOptions ) {
- var api = this;
-
- return api.getToken( tokenType, params.assert ).then( function ( token ) {
- params.token = token;
- return api.post( params, ajaxOptions ).then(
- // If no error, return to caller as-is
- null,
- // Error handler
- function ( code ) {
- if ( code === 'badtoken' ) {
- api.badToken( tokenType );
- // Try again, once
- params.token = undefined;
- return api.getToken( tokenType, params.assert ).then( function ( token ) {
- params.token = token;
- return api.post( params, ajaxOptions );
- } );
- }
-
- // Different error, pass on to let caller handle the error code
- return this;
- }
- );
- } );
- },
-
- /**
- * Get a token for a certain action from the API.
- *
- * The assert parameter is only for internal use by postWithToken.
- *
- * @param {string} type Token type
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string} return.done.token Received token.
- * @since 1.22
- */
- getToken: function ( type, assert ) {
- var apiPromise,
- promiseGroup = promises[ this.defaults.ajax.url ],
- d = promiseGroup && promiseGroup[ type + 'Token' ];
-
- if ( !d ) {
- apiPromise = this.get( { action: 'tokens', type: type, assert: assert } );
-
- d = apiPromise
- .then( function ( data ) {
- if ( data.tokens && data.tokens[ type + 'token' ] ) {
- return data.tokens[ type + 'token' ];
- }
-
- // If token type is not available for this user,
- // key '...token' is either missing or set to boolean false
- return $.Deferred().reject( 'token-missing', data );
- }, function () {
- // Clear promise. Do not cache errors.
- delete promiseGroup[ type + 'Token' ];
- // Pass on to allow the caller to handle the error
- return this;
- } )
- // Attach abort handler
- .promise( { abort: apiPromise.abort } );
-
- // Store deferred now so that we can use it again even if it isn't ready yet
- if ( !promiseGroup ) {
- promiseGroup = promises[ this.defaults.ajax.url ] = {};
- }
- promiseGroup[ type + 'Token' ] = d;
- }
-
- return d;
- },
-
- /**
- * Indicate that the cached token for a certain action of the API is bad.
- *
- * Call this if you get a 'badtoken' error when using the token returned by #getToken.
- * You may also want to use #postWithToken instead, which invalidates bad cached tokens
- * automatically.
- *
- * @param {string} type Token type
- * @since 1.26
- */
- badToken: function ( type ) {
- var promiseGroup = promises[ this.defaults.ajax.url ];
- if ( promiseGroup ) {
- delete promiseGroup[ type + 'Token' ];
- }
- }
- };
-
- /**
- * @static
- * @property {Array}
- * List of errors we might receive from the API.
- * For now, this just documents our expectation that there should be similar messages
- * available.
- */
- mw.Api.errors = [
- // occurs when POST aborted
- // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
- 'ok-but-empty',
-
- // timeout
- 'timeout',
-
- // really a warning, but we treat it like an error
- 'duplicate',
- 'duplicate-archive',
-
- // upload succeeded, but no image info.
- // this is probably impossible, but might as well check for it
- 'noimageinfo',
- // remote errors, defined in API
- 'uploaddisabled',
- 'nomodule',
- 'mustbeposted',
- 'badaccess-groups',
- 'missingresult',
- 'missingparam',
- 'invalid-file-key',
- 'copyuploaddisabled',
- 'mustbeloggedin',
- 'empty-file',
- 'file-too-large',
- 'filetype-missing',
- 'filetype-banned',
- 'filetype-banned-type',
- 'filename-tooshort',
- 'illegal-filename',
- 'verification-error',
- 'hookaborted',
- 'unknown-error',
- 'internal-error',
- 'overwrite',
- 'badtoken',
- 'fetchfileerror',
- 'fileexists-shared-forbidden',
- 'invalidtitle',
- 'notloggedin',
-
- // Stash-specific errors - expanded
- 'stashfailed',
- 'stasherror',
- 'stashedfilenotfound',
- 'stashpathinvalid',
- 'stashfilestorage',
- 'stashzerolength',
- 'stashnotloggedin',
- 'stashwrongowner',
- 'stashnosuchfilekey'
- ];
-
- /**
- * @static
- * @property {Array}
- * List of warnings we might receive from the API.
- * For now, this just documents our expectation that there should be similar messages
- * available.
- */
- mw.Api.warnings = [
- 'duplicate',
- 'exists'
- ];
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * Make the two-step login easier.
- *
- * @author Niklas Laxström
- * @class mw.Api.plugin.login
- * @since 1.22
- */
-( function ( mw, $ ) {
- 'use strict';
-
- $.extend( mw.Api.prototype, {
- /**
- * @param {string} username
- * @param {string} password
- * @return {jQuery.Promise} See mw.Api#post
- */
- login: function ( username, password ) {
- var params, apiPromise, innerPromise,
- api = this;
-
- params = {
- action: 'login',
- lgname: username,
- lgpassword: password
- };
-
- apiPromise = api.post( params );
-
- return apiPromise
- .then( function ( data ) {
- params.lgtoken = data.login.token;
- innerPromise = api.post( params )
- .then( function ( data ) {
- var code;
- if ( data.login.result !== 'Success' ) {
- // Set proper error code whenever possible
- code = data.error && data.error.code || 'unknown';
- return $.Deferred().reject( code, data );
- }
- return data;
- } );
- return innerPromise;
- } )
- .promise( {
- abort: function () {
- apiPromise.abort();
- if ( innerPromise ) {
- innerPromise.abort();
- }
- }
- } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.login
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.options
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
-
- /**
- * Asynchronously save the value of a single user option using the API. See #saveOptions.
- *
- * @param {string} name
- * @param {string|null} value
- * @return {jQuery.Promise}
- */
- saveOption: function ( name, value ) {
- var param = {};
- param[ name ] = value;
- return this.saveOptions( param );
- },
-
- /**
- * Asynchronously save the values of user options using the API.
- *
- * If a value of `null` is provided, the given option will be reset to the default value.
- *
- * Any warnings returned by the API, including warnings about invalid option names or values,
- * are ignored. However, do not rely on this behavior.
- *
- * If necessary, the options will be saved using several parallel API requests. Only one promise
- * is always returned that will be resolved when all requests complete.
- *
- * @param {Object} options Options as a `{ name: value, ⊠}` object
- * @return {jQuery.Promise}
- */
- saveOptions: function ( options ) {
- var name, value, bundleable,
- grouped = [],
- deferreds = [];
-
- for ( name in options ) {
- value = options[ name ] === null ? null : String( options[ name ] );
-
- // Can we bundle this option, or does it need a separate request?
- bundleable =
- ( value === null || value.indexOf( '|' ) === -1 ) &&
- ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
-
- if ( bundleable ) {
- if ( value !== null ) {
- grouped.push( name + '=' + value );
- } else {
- // Omitting value resets the option
- grouped.push( name );
- }
- } else {
- if ( value !== null ) {
- deferreds.push( this.postWithToken( 'options', {
- action: 'options',
- optionname: name,
- optionvalue: value
- } ) );
- } else {
- // Omitting value resets the option
- deferreds.push( this.postWithToken( 'options', {
- action: 'options',
- optionname: name
- } ) );
- }
- }
- }
-
- if ( grouped.length ) {
- deferreds.push( this.postWithToken( 'options', {
- action: 'options',
- change: grouped.join( '|' )
- } ) );
- }
-
- return $.when.apply( $, deferreds );
- }
-
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.options
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.parse
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Convenience method for 'action=parse'.
- *
- * @param {string} wikitext
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string} return.done.data Parsed HTML of `wikitext`.
- */
- parse: function ( wikitext ) {
- var apiPromise = this.get( {
- action: 'parse',
- contentmodel: 'wikitext',
- text: wikitext
- } );
-
- return apiPromise
- .then( function ( data ) {
- return data.parse.text[ '*' ];
- } )
- .promise( { abort: apiPromise.abort } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.parse
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * Provides an interface for uploading files to MediaWiki.
- *
- * @class mw.Api.plugin.upload
- * @singleton
- */
-( function ( mw, $ ) {
- var nonce = 0,
- fieldsAllowed = {
- stash: true,
- filekey: true,
- filename: true,
- comment: true,
- text: true,
- watchlist: true,
- ignorewarnings: true
- };
-
- /**
- * @private
- * Get nonce for iframe IDs on the page.
- *
- * @return {number}
- */
- function getNonce() {
- return nonce++;
- }
-
- /**
- * @private
- * Get new iframe object for an upload.
- *
- * @return {HTMLIframeElement}
- */
- function getNewIframe( id ) {
- var frame = document.createElement( 'iframe' );
- frame.id = id;
- frame.name = id;
- return frame;
- }
-
- /**
- * @private
- * Shortcut for getting hidden inputs
- *
- * @return {jQuery}
- */
- function getHiddenInput( name, val ) {
- return $( '<input type="hidden" />' )
- .attr( 'name', name )
- .val( val );
- }
-
- /**
- * Process the result of the form submission, returned to an iframe.
- * This is the iframe's onload event.
- *
- * @param {HTMLIframeElement} iframe Iframe to extract result from
- * @return {Object} Response from the server. The return value may or may
- * not be an XMLDocument, this code was copied from elsewhere, so if you
- * see an unexpected return type, please file a bug.
- */
- function processIframeResult( iframe ) {
- var json,
- doc = iframe.contentDocument || frames[ iframe.id ].document;
-
- if ( doc.XMLDocument ) {
- // The response is a document property in IE
- return doc.XMLDocument;
- }
-
- if ( doc.body ) {
- // Get the json string
- // We're actually searching through an HTML doc here --
- // according to mdale we need to do this
- // because IE does not load JSON properly in an iframe
- json = $( doc.body ).find( 'pre' ).text();
-
- return JSON.parse( json );
- }
-
- // Response is a xml document
- return doc;
- }
-
- function formDataAvailable() {
- return window.FormData !== undefined &&
- window.File !== undefined &&
- window.File.prototype.slice !== undefined;
- }
-
- $.extend( mw.Api.prototype, {
- /**
- * Upload a file to MediaWiki.
- *
- * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
- * iframe if it doesn't.
- *
- * Caveats of iframe upload:
- * - The returned jQuery.Promise will not receive `progress` notifications during the upload
- * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
- * - You must pass a HTMLInputElement and not a File for it to be possible
- *
- * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside
- * of it, or a File object.
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- upload: function ( file, data ) {
- var isFileInput, canUseFormData;
-
- isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
-
- if ( formDataAvailable() && isFileInput && file.files ) {
- file = file.files[ 0 ];
- }
-
- if ( !file ) {
- return $.Deferred().reject( 'No file' );
- }
-
- canUseFormData = formDataAvailable() && file instanceof window.File;
-
- if ( !isFileInput && !canUseFormData ) {
- return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
- }
-
- if ( canUseFormData ) {
- return this.uploadWithFormData( file, data );
- }
-
- return this.uploadWithIframe( file, data );
- },
-
- /**
- * Upload a file to MediaWiki with an iframe and a form.
- *
- * This method is necessary for browsers without the File/FormData
- * APIs, and continues to work in browsers with those APIs.
- *
- * The rough sketch of how this method works is as follows:
- * 1. An iframe is loaded with no content.
- * 2. A form is submitted with the passed-in file input and some extras.
- * 3. The MediaWiki API receives that form data, and sends back a response.
- * 4. The response is sent to the iframe, because we set target=(iframe id)
- * 5. The response is parsed out of the iframe's document, and passed back
- * through the promise.
- *
- * @private
- * @param {HTMLInputElement} file The file input with a file in it.
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- uploadWithIframe: function ( file, data ) {
- var key,
- tokenPromise = $.Deferred(),
- api = this,
- deferred = $.Deferred(),
- nonce = getNonce(),
- id = 'uploadframe-' + nonce,
- $form = $( '<form>' ),
- iframe = getNewIframe( id ),
- $iframe = $( iframe );
-
- for ( key in data ) {
- if ( !fieldsAllowed[ key ] ) {
- delete data[ key ];
- }
- }
-
- data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
- $form.addClass( 'mw-api-upload-form' );
-
- $form.css( 'display', 'none' )
- .attr( {
- action: this.defaults.ajax.url,
- method: 'POST',
- target: id,
- enctype: 'multipart/form-data'
- } );
-
- $iframe.one( 'load', function () {
- $iframe.one( 'load', function () {
- var result = processIframeResult( iframe );
-
- if ( !result ) {
- deferred.reject( 'No response from API on upload attempt.' );
- } else if ( result.error || result.warnings ) {
- if ( result.error && result.error.code === 'badtoken' ) {
- api.badToken( 'edit' );
- }
-
- deferred.reject( result.error || result.warnings );
- } else {
- deferred.notify( 1 );
- deferred.resolve( result );
- }
- } );
- tokenPromise.done( function () {
- $form.submit();
- } );
- } );
-
- $iframe.error( function ( error ) {
- deferred.reject( 'iframe failed to load: ' + error );
- } );
-
- $iframe.prop( 'src', 'about:blank' ).hide();
-
- file.name = 'file';
-
- $.each( data, function ( key, val ) {
- $form.append( getHiddenInput( key, val ) );
- } );
-
- if ( !data.filename && !data.stash ) {
- return $.Deferred().reject( 'Filename not included in file data.' );
- }
-
- if ( this.needToken() ) {
- this.getEditToken().then( function ( token ) {
- $form.append( getHiddenInput( 'token', token ) );
- tokenPromise.resolve();
- }, tokenPromise.reject );
- } else {
- tokenPromise.resolve();
- }
-
- $( 'body' ).append( $form, $iframe );
-
- deferred.always( function () {
- $form.remove();
- $iframe.remove();
- } );
-
- return deferred.promise();
- },
-
- /**
- * Uploads a file using the FormData API.
- *
- * @private
- * @param {File} file
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- uploadWithFormData: function ( file, data ) {
- var key,
- deferred = $.Deferred();
-
- for ( key in data ) {
- if ( !fieldsAllowed[ key ] ) {
- delete data[ key ];
- }
- }
-
- data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
- data.file = file;
-
- if ( !data.filename && !data.stash ) {
- return $.Deferred().reject( 'Filename not included in file data.' );
- }
-
- // Use this.postWithEditToken() or this.post()
- this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
- // Use FormData (if we got here, we know that it's available)
- contentType: 'multipart/form-data',
- // Provide upload progress notifications
- xhr: function () {
- var xhr = $.ajaxSettings.xhr();
- if ( xhr.upload ) {
- // need to bind this event before we open the connection (see note at
- // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
- xhr.upload.addEventListener( 'progress', function ( ev ) {
- if ( ev.lengthComputable ) {
- deferred.notify( ev.loaded / ev.total );
- }
- } );
- }
- return xhr;
- }
- } )
- .done( function ( result ) {
- if ( result.error || result.warnings ) {
- deferred.reject( result.error || result.warnings );
- } else {
- deferred.notify( 1 );
- deferred.resolve( result );
- }
- } )
- .fail( function ( result ) {
- deferred.reject( result );
- } );
-
- return deferred.promise();
- },
-
- /**
- * Upload a file to the stash.
- *
- * This function will return a promise, which when resolved, will pass back a function
- * to finish the stash upload. You can call that function with an argument containing
- * more, or conflicting, data to pass to the server. For example:
- *
- * // upload a file to the stash with a placeholder filename
- * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
- * // finish is now the function we can use to finalize the upload
- * // pass it a new filename from user input to override the initial value
- * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
- * // the upload is complete, data holds the API response
- * } );
- * } );
- *
- * @param {File|HTMLInputElement} file
- * @param {Object} [data]
- * @return {jQuery.Promise}
- * @return {Function} return.finishStashUpload Call this function to finish the upload.
- * @return {Object} return.finishStashUpload.data Additional data for the upload.
- * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
- * @return {Object} return.finishStashUpload.return.data API return value for the final upload
- */
- uploadToStash: function ( file, data ) {
- var filekey,
- api = this;
-
- if ( !data.filename ) {
- return $.Deferred().reject( 'Filename not included in file data.' );
- }
-
- function finishUpload( moreData ) {
- data = $.extend( data, moreData );
- data.filekey = filekey;
- data.action = 'upload';
- data.format = 'json';
-
- if ( !data.filename ) {
- return $.Deferred().reject( 'Filename not included in file data.' );
- }
-
- return api.postWithEditToken( data ).then( function ( result ) {
- if ( result.upload && ( result.upload.error || result.upload.warnings ) ) {
- return $.Deferred().reject( result.upload.error || result.upload.warnings ).promise();
- }
- return result;
- } );
- }
-
- return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) {
- if ( result && result.upload && result.upload.filekey ) {
- filekey = result.upload.filekey;
- } else if ( result && ( result.error || result.warning ) ) {
- return $.Deferred().reject( result );
- }
-
- return finishUpload;
- } );
- },
-
- needToken: function () {
- return true;
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.upload
- */
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.watch
- * @since 1.19
- */
-( function ( mw, $ ) {
-
- /**
- * @private
- * @static
- * @context mw.Api
- *
- * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
- * array thereof. If an array is passed, the return value passed to the promise will also be an
- * array of appropriate objects.
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
- * parameter)
- * @return {string} return.done.watch.title Full pagename
- * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
- * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message
- */
- function doWatchInternal( pages, addParams ) {
- // XXX: Parameter addParams is undocumented because we inherit this
- // documentation in the public method...
- var apiPromise = this.postWithToken( 'watch',
- $.extend(
- {
- action: 'watch',
- titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ),
- uselang: mw.config.get( 'wgUserLanguage' )
- },
- addParams
- )
- );
-
- return apiPromise
- .then( function ( data ) {
- // If a single page was given (not an array) respond with a single item as well.
- return $.isArray( pages ) ? data.watch : data.watch[ 0 ];
- } )
- .promise( { abort: apiPromise.abort } );
- }
-
- $.extend( mw.Api.prototype, {
- /**
- * Convenience method for `action=watch`.
- *
- * @inheritdoc #doWatchInternal
- */
- watch: function ( pages ) {
- return doWatchInternal.call( this, pages );
- },
-
- /**
- * Convenience method for `action=watch&unwatch=1`.
- *
- * @inheritdoc #doWatchInternal
- */
- unwatch: function ( pages ) {
- return doWatchInternal.call( this, pages, { unwatch: 1 } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.watch
- */
-
-}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+
+ /**
+ * Create an object like mw.Api, but automatically handling everything required to communicate
+ * with another MediaWiki wiki via cross-origin requests (CORS).
+ *
+ * The foreign wiki must be configured to accept requests from the current wiki. See
+ * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> for details.
+ *
+ * var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' );
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter
+ * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this
+ * doesn't guarantee that it's the same user.)
+ *
+ * Authentication-related MediaWiki extensions may extend this class to ensure that the user
+ * authenticated on the current wiki will be automatically authenticated on the foreign one. These
+ * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See
+ * CentralAuth for a practical example. The general pattern to extend and override the name is:
+ *
+ * function MyForeignApi() {};
+ * OO.inheritClass( MyForeignApi, mw.ForeignApi );
+ * mw.ForeignApi = MyForeignApi;
+ *
+ * @class mw.ForeignApi
+ * @extends mw.Api
+ * @since 1.26
+ *
+ * @constructor
+ * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint.
+ * @param {Object} [options] See mw.Api.
+ *
+ * @author Bartosz DziewoĆski
+ * @author Jon Robson
+ */
+ function CoreForeignApi( url, options ) {
+ if ( !url || $.isPlainObject( url ) ) {
+ throw new Error( 'mw.ForeignApi() requires a `url` parameter' );
+ }
+
+ this.apiUrl = String( url );
+
+ options = $.extend( /*deep=*/ true,
+ {
+ ajax: {
+ url: this.apiUrl,
+ xhrFields: {
+ withCredentials: true
+ }
+ },
+ parameters: {
+ // Add 'origin' query parameter to all requests.
+ origin: this.getOrigin()
+ }
+ },
+ options
+ );
+
+ // Call parent constructor
+ CoreForeignApi.parent.call( this, options );
+ }
+
+ OO.inheritClass( CoreForeignApi, mw.Api );
+
+ /**
+ * Return the origin to use for API requests, in the required format (protocol, host and port, if
+ * any).
+ *
+ * @protected
+ * @return {string}
+ */
+ CoreForeignApi.prototype.getOrigin = function () {
+ var origin = location.protocol + '//' + location.hostname;
+ if ( location.port ) {
+ origin += ':' + location.port;
+ }
+ return origin;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) {
+ var url, origin, newAjaxOptions;
+
+ // 'origin' query parameter must be part of the request URI, and not just POST request body
+ if ( ajaxOptions.type === 'POST' ) {
+ url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url;
+ origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin;
+ url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) +
+ 'origin=' + encodeURIComponent( origin );
+ newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } );
+ } else {
+ newAjaxOptions = ajaxOptions;
+ }
+
+ return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions );
+ };
+
+ // Expose
+ mw.ForeignApi = CoreForeignApi;
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+
+ /**
+ * @class mw.Api
+ */
+
+ /**
+ * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
+ * `options` to mw.Api constructor.
+ * @property {Object} defaultOptions.parameters Default query parameters for API requests.
+ * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
+ * @private
+ */
+ var defaultOptions = {
+ parameters: {
+ action: 'query',
+ format: 'json'
+ },
+ ajax: {
+ url: mw.util.wikiScript( 'api' ),
+ timeout: 30 * 1000, // 30 seconds
+ dataType: 'json'
+ }
+ },
+
+ // Keyed by ajax url and symbolic name for the individual request
+ promises = {};
+
+ // Pre-populate with fake ajax promises to save http requests for tokens
+ // we already have on the page via the user.tokens module (bug 34733).
+ promises[ defaultOptions.ajax.url ] = {};
+ $.each( mw.user.tokens.get(), function ( key, value ) {
+ // This requires #getToken to use the same key as user.tokens.
+ // Format: token-type + "Token" (eg. editToken, patrolToken, watchToken).
+ promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
+ .resolve( value )
+ .promise( { abort: function () {} } );
+ } );
+
+ /**
+ * Constructor to create an object to interact with the API of a particular MediaWiki server.
+ * mw.Api objects represent the API of a particular MediaWiki server.
+ *
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * Since MW 1.25, multiple values for a parameter can be specified using an array:
+ *
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
+ * } ).done( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
+ * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
+ *
+ * @constructor
+ * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
+ * each individual request by passing them to #get or #post (or directly #ajax) later on.
+ */
+ mw.Api = function ( options ) {
+ // TODO: Share API objects with exact same config.
+ options = options || {};
+
+ // Force a string if we got a mw.Uri object
+ if ( options.ajax && options.ajax.url !== undefined ) {
+ options.ajax.url = String( options.ajax.url );
+ }
+
+ options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
+ options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
+
+ this.defaults = options;
+ };
+
+ mw.Api.prototype = {
+
+ /**
+ * Perform API get request
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ get: function ( parameters, ajaxOptions ) {
+ ajaxOptions = ajaxOptions || {};
+ ajaxOptions.type = 'GET';
+ return this.ajax( parameters, ajaxOptions );
+ },
+
+ /**
+ * Perform API post request
+ *
+ * TODO: Post actions for non-local hostnames will need proxy.
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ post: function ( parameters, ajaxOptions ) {
+ ajaxOptions = ajaxOptions || {};
+ ajaxOptions.type = 'POST';
+ return this.ajax( parameters, ajaxOptions );
+ },
+
+ /**
+ * Massage parameters from the nice format we accept into a format suitable for the API.
+ *
+ * @private
+ * @param {Object} parameters (modified in-place)
+ */
+ preprocessParameters: function ( parameters ) {
+ var key;
+ // Handle common MediaWiki API idioms for passing parameters
+ for ( key in parameters ) {
+ // Multiple values are pipe-separated
+ if ( $.isArray( parameters[ key ] ) ) {
+ parameters[ key ] = parameters[ key ].join( '|' );
+ }
+ // Boolean values are only false when not given at all
+ if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
+ delete parameters[ key ];
+ }
+ }
+ },
+
+ /**
+ * Perform the API call.
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} Done: API response data and the jqXHR object.
+ * Fail: Error code
+ */
+ ajax: function ( parameters, ajaxOptions ) {
+ var token,
+ apiDeferred = $.Deferred(),
+ xhr, key, formData;
+
+ parameters = $.extend( {}, this.defaults.parameters, parameters );
+ ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
+
+ // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
+ if ( parameters.token ) {
+ token = parameters.token;
+ delete parameters.token;
+ }
+
+ this.preprocessParameters( parameters );
+
+ // If multipart/form-data has been requested and emulation is possible, emulate it
+ if (
+ ajaxOptions.type === 'POST' &&
+ window.FormData &&
+ ajaxOptions.contentType === 'multipart/form-data'
+ ) {
+
+ formData = new FormData();
+
+ for ( key in parameters ) {
+ formData.append( key, parameters[ key ] );
+ }
+ // If we extracted a token parameter, add it back in.
+ if ( token ) {
+ formData.append( 'token', token );
+ }
+
+ ajaxOptions.data = formData;
+
+ // Prevent jQuery from mangling our FormData object
+ ajaxOptions.processData = false;
+ // Prevent jQuery from overriding the Content-Type header
+ ajaxOptions.contentType = false;
+ } else {
+ // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug
+ // So let's escape them here. See bug #28235
+ // This works because jQuery accepts data as a query string or as an Object
+ ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' );
+
+ // If we extracted a token parameter, add it back in.
+ if ( token ) {
+ ajaxOptions.data += '&token=' + encodeURIComponent( token );
+ }
+
+ if ( ajaxOptions.contentType === 'multipart/form-data' ) {
+ // We were asked to emulate but can't, so drop the Content-Type header, otherwise
+ // it'll be wrong and the server will fail to decode the POST body
+ delete ajaxOptions.contentType;
+ }
+ }
+
+ // Make the AJAX request
+ xhr = $.ajax( ajaxOptions )
+ // If AJAX fails, reject API call with error code 'http'
+ // and details in second argument.
+ .fail( function ( xhr, textStatus, exception ) {
+ apiDeferred.reject( 'http', {
+ xhr: xhr,
+ textStatus: textStatus,
+ exception: exception
+ } );
+ } )
+ // AJAX success just means "200 OK" response, also check API error codes
+ .done( function ( result, textStatus, jqXHR ) {
+ if ( result === undefined || result === null || result === '' ) {
+ apiDeferred.reject( 'ok-but-empty',
+ 'OK response but empty result (check HTTP headers?)'
+ );
+ } else if ( result.error ) {
+ var code = result.error.code === undefined ? 'unknown' : result.error.code;
+ apiDeferred.reject( code, result );
+ } else {
+ apiDeferred.resolve( result, jqXHR );
+ }
+ } );
+
+ // Return the Promise
+ return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
+ if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
+ mw.log( 'mw.Api error: ', code, details );
+ }
+ } );
+ },
+
+ /**
+ * Post to API with specified type of token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over. For example to change an user option you could do:
+ *
+ * new mw.Api().postWithToken( 'options', {
+ * action: 'options',
+ * optionname: 'gender',
+ * optionvalue: 'female'
+ * } );
+ *
+ * @param {string} tokenType The name of the token, like options or edit.
+ * @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} See #post
+ * @since 1.22
+ */
+ postWithToken: function ( tokenType, params, ajaxOptions ) {
+ var api = this;
+
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
+ params.token = token;
+ return api.post( params, ajaxOptions ).then(
+ // If no error, return to caller as-is
+ null,
+ // Error handler
+ function ( code ) {
+ if ( code === 'badtoken' ) {
+ api.badToken( tokenType );
+ // Try again, once
+ params.token = undefined;
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
+ params.token = token;
+ return api.post( params, ajaxOptions );
+ } );
+ }
+
+ // Different error, pass on to let caller handle the error code
+ return this;
+ }
+ );
+ } );
+ },
+
+ /**
+ * Get a token for a certain action from the API.
+ *
+ * The assert parameter is only for internal use by postWithToken.
+ *
+ * @param {string} type Token type
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
+ * @since 1.22
+ */
+ getToken: function ( type, assert ) {
+ var apiPromise,
+ promiseGroup = promises[ this.defaults.ajax.url ],
+ d = promiseGroup && promiseGroup[ type + 'Token' ];
+
+ if ( !d ) {
+ apiPromise = this.get( { action: 'tokens', type: type, assert: assert } );
+
+ d = apiPromise
+ .then( function ( data ) {
+ if ( data.tokens && data.tokens[ type + 'token' ] ) {
+ return data.tokens[ type + 'token' ];
+ }
+
+ // If token type is not available for this user,
+ // key '...token' is either missing or set to boolean false
+ return $.Deferred().reject( 'token-missing', data );
+ }, function () {
+ // Clear promise. Do not cache errors.
+ delete promiseGroup[ type + 'Token' ];
+ // Pass on to allow the caller to handle the error
+ return this;
+ } )
+ // Attach abort handler
+ .promise( { abort: apiPromise.abort } );
+
+ // Store deferred now so that we can use it again even if it isn't ready yet
+ if ( !promiseGroup ) {
+ promiseGroup = promises[ this.defaults.ajax.url ] = {};
+ }
+ promiseGroup[ type + 'Token' ] = d;
+ }
+
+ return d;
+ },
+
+ /**
+ * Indicate that the cached token for a certain action of the API is bad.
+ *
+ * Call this if you get a 'badtoken' error when using the token returned by #getToken.
+ * You may also want to use #postWithToken instead, which invalidates bad cached tokens
+ * automatically.
+ *
+ * @param {string} type Token type
+ * @since 1.26
+ */
+ badToken: function ( type ) {
+ var promiseGroup = promises[ this.defaults.ajax.url ];
+ if ( promiseGroup ) {
+ delete promiseGroup[ type + 'Token' ];
+ }
+ }
+ };
+
+ /**
+ * @static
+ * @property {Array}
+ * List of errors we might receive from the API.
+ * For now, this just documents our expectation that there should be similar messages
+ * available.
+ */
+ mw.Api.errors = [
+ // occurs when POST aborted
+ // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
+ 'ok-but-empty',
+
+ // timeout
+ 'timeout',
+
+ // really a warning, but we treat it like an error
+ 'duplicate',
+ 'duplicate-archive',
+
+ // upload succeeded, but no image info.
+ // this is probably impossible, but might as well check for it
+ 'noimageinfo',
+ // remote errors, defined in API
+ 'uploaddisabled',
+ 'nomodule',
+ 'mustbeposted',
+ 'badaccess-groups',
+ 'missingresult',
+ 'missingparam',
+ 'invalid-file-key',
+ 'copyuploaddisabled',
+ 'mustbeloggedin',
+ 'empty-file',
+ 'file-too-large',
+ 'filetype-missing',
+ 'filetype-banned',
+ 'filetype-banned-type',
+ 'filename-tooshort',
+ 'illegal-filename',
+ 'verification-error',
+ 'hookaborted',
+ 'unknown-error',
+ 'internal-error',
+ 'overwrite',
+ 'badtoken',
+ 'fetchfileerror',
+ 'fileexists-shared-forbidden',
+ 'invalidtitle',
+ 'notloggedin',
+
+ // Stash-specific errors - expanded
+ 'stashfailed',
+ 'stasherror',
+ 'stashedfilenotfound',
+ 'stashpathinvalid',
+ 'stashfilestorage',
+ 'stashzerolength',
+ 'stashnotloggedin',
+ 'stashwrongowner',
+ 'stashnosuchfilekey'
+ ];
+
+ /**
+ * @static
+ * @property {Array}
+ * List of warnings we might receive from the API.
+ * For now, this just documents our expectation that there should be similar messages
+ * available.
+ */
+ mw.Api.warnings = [
+ 'duplicate',
+ 'exists'
+ ];
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.category
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Determine if a category exists.
+ *
+ * @param {mw.Title|string} title
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean} return.done.isCategory Whether the category exists.
+ */
+ isCategory: function ( title ) {
+ var apiPromise = this.get( {
+ prop: 'categoryinfo',
+ titles: String( title )
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ var exists = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categoryinfo ) {
+ exists = true;
+ }
+ } );
+ }
+ return exists;
+ } )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get a list of categories that match a certain prefix.
+ *
+ * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
+ *
+ * @param {string} prefix Prefix to match.
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string[]} return.done.categories Matched categories
+ */
+ getCategoriesByPrefix: function ( prefix ) {
+ // Fetch with allpages to only get categories that have a corresponding description page.
+ var apiPromise = this.get( {
+ list: 'allpages',
+ apprefix: prefix,
+ apnamespace: mw.config.get( 'wgNamespaceIds' ).category
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ var texts = [];
+ if ( data.query && data.query.allpages ) {
+ $.each( data.query.allpages, function ( i, category ) {
+ texts.push( new mw.Title( category.title ).getMainText() );
+ } );
+ }
+ return texts;
+ } )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get the categories that a particular page on the wiki belongs to.
+ *
+ * @param {mw.Title|string} title
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
+ * if title was not found.
+ */
+ getCategories: function ( title ) {
+ var apiPromise = this.get( {
+ prop: 'categories',
+ titles: String( title )
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ var titles = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categories ) {
+ if ( titles === false ) {
+ titles = [];
+ }
+ $.each( page.categories, function ( i, cat ) {
+ titles.push( new mw.Title( cat.title ) );
+ } );
+ }
+ } );
+ }
+ return titles;
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.category
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.edit
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Post to API with edit token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over.
+ *
+ * @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} See #post
+ */
+ postWithEditToken: function ( params, ajaxOptions ) {
+ return this.postWithToken( 'edit', params, ajaxOptions );
+ },
+
+ /**
+ * API helper to grab an edit token.
+ *
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
+ */
+ getEditToken: function () {
+ return this.getToken( 'edit' );
+ },
+
+ /**
+ * Post a new section to the page.
+ *
+ * @see #postWithEditToken
+ * @param {mw.Title|String} title Target page
+ * @param {string} header
+ * @param {string} message wikitext message
+ * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
+ * @return {jQuery.Promise}
+ */
+ newSection: function ( title, header, message, additionalParams ) {
+ return this.postWithEditToken( $.extend( {
+ action: 'edit',
+ section: 'new',
+ title: String( title ),
+ summary: header,
+ text: message
+ }, additionalParams ) );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.edit
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * Make the two-step login easier.
+ *
+ * @author Niklas Laxström
+ * @class mw.Api.plugin.login
+ * @since 1.22
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * @param {string} username
+ * @param {string} password
+ * @return {jQuery.Promise} See mw.Api#post
+ */
+ login: function ( username, password ) {
+ var params, apiPromise, innerPromise,
+ api = this;
+
+ params = {
+ action: 'login',
+ lgname: username,
+ lgpassword: password
+ };
+
+ apiPromise = api.post( params );
+
+ return apiPromise
+ .then( function ( data ) {
+ params.lgtoken = data.login.token;
+ innerPromise = api.post( params )
+ .then( function ( data ) {
+ var code;
+ if ( data.login.result !== 'Success' ) {
+ // Set proper error code whenever possible
+ code = data.error && data.error.code || 'unknown';
+ return $.Deferred().reject( code, data );
+ }
+ return data;
+ } );
+ return innerPromise;
+ } )
+ .promise( {
+ abort: function () {
+ apiPromise.abort();
+ if ( innerPromise ) {
+ innerPromise.abort();
+ }
+ }
+ } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.login
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.options
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Asynchronously save the value of a single user option using the API. See #saveOptions.
+ *
+ * @param {string} name
+ * @param {string|null} value
+ * @return {jQuery.Promise}
+ */
+ saveOption: function ( name, value ) {
+ var param = {};
+ param[ name ] = value;
+ return this.saveOptions( param );
+ },
+
+ /**
+ * Asynchronously save the values of user options using the API.
+ *
+ * If a value of `null` is provided, the given option will be reset to the default value.
+ *
+ * Any warnings returned by the API, including warnings about invalid option names or values,
+ * are ignored. However, do not rely on this behavior.
+ *
+ * If necessary, the options will be saved using several parallel API requests. Only one promise
+ * is always returned that will be resolved when all requests complete.
+ *
+ * @param {Object} options Options as a `{ name: value, ⊠}` object
+ * @return {jQuery.Promise}
+ */
+ saveOptions: function ( options ) {
+ var name, value, bundleable,
+ grouped = [],
+ deferreds = [];
+
+ for ( name in options ) {
+ value = options[ name ] === null ? null : String( options[ name ] );
+
+ // Can we bundle this option, or does it need a separate request?
+ bundleable =
+ ( value === null || value.indexOf( '|' ) === -1 ) &&
+ ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+
+ if ( bundleable ) {
+ if ( value !== null ) {
+ grouped.push( name + '=' + value );
+ } else {
+ // Omitting value resets the option
+ grouped.push( name );
+ }
+ } else {
+ if ( value !== null ) {
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ optionname: name,
+ optionvalue: value
+ } ) );
+ } else {
+ // Omitting value resets the option
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ optionname: name
+ } ) );
+ }
+ }
+ }
+
+ if ( grouped.length ) {
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ change: grouped.join( '|' )
+ } ) );
+ }
+
+ return $.when.apply( $, deferreds );
+ }
+
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.options
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.parse
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for 'action=parse'.
+ *
+ * @param {string} wikitext
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.data Parsed HTML of `wikitext`.
+ */
+ parse: function ( wikitext ) {
+ var apiPromise = this.get( {
+ action: 'parse',
+ contentmodel: 'wikitext',
+ text: wikitext
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ return data.parse.text[ '*' ];
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.parse
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * Provides an interface for uploading files to MediaWiki.
+ *
+ * @class mw.Api.plugin.upload
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var nonce = 0,
+ fieldsAllowed = {
+ stash: true,
+ filekey: true,
+ filename: true,
+ comment: true,
+ text: true,
+ watchlist: true,
+ ignorewarnings: true
+ };
+
+ /**
+ * @private
+ * Get nonce for iframe IDs on the page.
+ *
+ * @return {number}
+ */
+ function getNonce() {
+ return nonce++;
+ }
+
+ /**
+ * @private
+ * Get new iframe object for an upload.
+ *
+ * @return {HTMLIframeElement}
+ */
+ function getNewIframe( id ) {
+ var frame = document.createElement( 'iframe' );
+ frame.id = id;
+ frame.name = id;
+ return frame;
+ }
+
+ /**
+ * @private
+ * Shortcut for getting hidden inputs
+ *
+ * @return {jQuery}
+ */
+ function getHiddenInput( name, val ) {
+ return $( '<input type="hidden" />' )
+ .attr( 'name', name )
+ .val( val );
+ }
+
+ /**
+ * Process the result of the form submission, returned to an iframe.
+ * This is the iframe's onload event.
+ *
+ * @param {HTMLIframeElement} iframe Iframe to extract result from
+ * @return {Object} Response from the server. The return value may or may
+ * not be an XMLDocument, this code was copied from elsewhere, so if you
+ * see an unexpected return type, please file a bug.
+ */
+ function processIframeResult( iframe ) {
+ var json,
+ doc = iframe.contentDocument || frames[ iframe.id ].document;
+
+ if ( doc.XMLDocument ) {
+ // The response is a document property in IE
+ return doc.XMLDocument;
+ }
+
+ if ( doc.body ) {
+ // Get the json string
+ // We're actually searching through an HTML doc here --
+ // according to mdale we need to do this
+ // because IE does not load JSON properly in an iframe
+ json = $( doc.body ).find( 'pre' ).text();
+
+ return JSON.parse( json );
+ }
+
+ // Response is a xml document
+ return doc;
+ }
+
+ function formDataAvailable() {
+ return window.FormData !== undefined &&
+ window.File !== undefined &&
+ window.File.prototype.slice !== undefined;
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Upload a file to MediaWiki.
+ *
+ * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
+ * iframe if it doesn't.
+ *
+ * Caveats of iframe upload:
+ * - The returned jQuery.Promise will not receive `progress` notifications during the upload
+ * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
+ * - You must pass a HTMLInputElement and not a File for it to be possible
+ *
+ * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside
+ * of it, or a File object.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ upload: function ( file, data ) {
+ var isFileInput, canUseFormData;
+
+ isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
+
+ if ( formDataAvailable() && isFileInput && file.files ) {
+ file = file.files[ 0 ];
+ }
+
+ if ( !file ) {
+ return $.Deferred().reject( 'No file' );
+ }
+
+ canUseFormData = formDataAvailable() && file instanceof window.File;
+
+ if ( !isFileInput && !canUseFormData ) {
+ return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
+ }
+
+ if ( canUseFormData ) {
+ return this.uploadWithFormData( file, data );
+ }
+
+ return this.uploadWithIframe( file, data );
+ },
+
+ /**
+ * Upload a file to MediaWiki with an iframe and a form.
+ *
+ * This method is necessary for browsers without the File/FormData
+ * APIs, and continues to work in browsers with those APIs.
+ *
+ * The rough sketch of how this method works is as follows:
+ * 1. An iframe is loaded with no content.
+ * 2. A form is submitted with the passed-in file input and some extras.
+ * 3. The MediaWiki API receives that form data, and sends back a response.
+ * 4. The response is sent to the iframe, because we set target=(iframe id)
+ * 5. The response is parsed out of the iframe's document, and passed back
+ * through the promise.
+ *
+ * @private
+ * @param {HTMLInputElement} file The file input with a file in it.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithIframe: function ( file, data ) {
+ var key,
+ tokenPromise = $.Deferred(),
+ api = this,
+ deferred = $.Deferred(),
+ nonce = getNonce(),
+ id = 'uploadframe-' + nonce,
+ $form = $( '<form>' ),
+ iframe = getNewIframe( id ),
+ $iframe = $( iframe );
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ $form.addClass( 'mw-api-upload-form' );
+
+ $form.css( 'display', 'none' )
+ .attr( {
+ action: this.defaults.ajax.url,
+ method: 'POST',
+ target: id,
+ enctype: 'multipart/form-data'
+ } );
+
+ $iframe.one( 'load', function () {
+ $iframe.one( 'load', function () {
+ var result = processIframeResult( iframe );
+
+ if ( !result ) {
+ deferred.reject( 'No response from API on upload attempt.' );
+ } else if ( result.error || result.warnings ) {
+ if ( result.error && result.error.code === 'badtoken' ) {
+ api.badToken( 'edit' );
+ }
+
+ deferred.reject( result.error || result.warnings );
+ } else {
+ deferred.notify( 1 );
+ deferred.resolve( result );
+ }
+ } );
+ tokenPromise.done( function () {
+ $form.submit();
+ } );
+ } );
+
+ $iframe.error( function ( error ) {
+ deferred.reject( 'iframe failed to load: ' + error );
+ } );
+
+ $iframe.prop( 'src', 'about:blank' ).hide();
+
+ file.name = 'file';
+
+ $.each( data, function ( key, val ) {
+ $form.append( getHiddenInput( key, val ) );
+ } );
+
+ if ( !data.filename && !data.stash ) {
+ return $.Deferred().reject( 'Filename not included in file data.' );
+ }
+
+ if ( this.needToken() ) {
+ this.getEditToken().then( function ( token ) {
+ $form.append( getHiddenInput( 'token', token ) );
+ tokenPromise.resolve();
+ }, tokenPromise.reject );
+ } else {
+ tokenPromise.resolve();
+ }
+
+ $( 'body' ).append( $form, $iframe );
+
+ deferred.always( function () {
+ $form.remove();
+ $iframe.remove();
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Uploads a file using the FormData API.
+ *
+ * @private
+ * @param {File} file
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithFormData: function ( file, data ) {
+ var key,
+ deferred = $.Deferred();
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ data.file = file;
+
+ if ( !data.filename && !data.stash ) {
+ return $.Deferred().reject( 'Filename not included in file data.' );
+ }
+
+ // Use this.postWithEditToken() or this.post()
+ this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
+ // Use FormData (if we got here, we know that it's available)
+ contentType: 'multipart/form-data',
+ // Provide upload progress notifications
+ xhr: function () {
+ var xhr = $.ajaxSettings.xhr();
+ if ( xhr.upload ) {
+ // need to bind this event before we open the connection (see note at
+ // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
+ xhr.upload.addEventListener( 'progress', function ( ev ) {
+ if ( ev.lengthComputable ) {
+ deferred.notify( ev.loaded / ev.total );
+ }
+ } );
+ }
+ return xhr;
+ }
+ } )
+ .done( function ( result ) {
+ if ( result.error || result.warnings ) {
+ deferred.reject( result.error || result.warnings );
+ } else {
+ deferred.notify( 1 );
+ deferred.resolve( result );
+ }
+ } )
+ .fail( function ( result ) {
+ deferred.reject( result );
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Upload a file to the stash.
+ *
+ * This function will return a promise, which when resolved, will pass back a function
+ * to finish the stash upload. You can call that function with an argument containing
+ * more, or conflicting, data to pass to the server. For example:
+ *
+ * // upload a file to the stash with a placeholder filename
+ * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
+ * // finish is now the function we can use to finalize the upload
+ * // pass it a new filename from user input to override the initial value
+ * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
+ * // the upload is complete, data holds the API response
+ * } );
+ * } );
+ *
+ * @param {File|HTMLInputElement} file
+ * @param {Object} [data]
+ * @return {jQuery.Promise}
+ * @return {Function} return.finishStashUpload Call this function to finish the upload.
+ * @return {Object} return.finishStashUpload.data Additional data for the upload.
+ * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
+ * @return {Object} return.finishStashUpload.return.data API return value for the final upload
+ */
+ uploadToStash: function ( file, data ) {
+ var filekey,
+ api = this;
+
+ if ( !data.filename ) {
+ return $.Deferred().reject( 'Filename not included in file data.' );
+ }
+
+ function finishUpload( moreData ) {
+ data = $.extend( data, moreData );
+ data.filekey = filekey;
+ data.action = 'upload';
+ data.format = 'json';
+
+ if ( !data.filename ) {
+ return $.Deferred().reject( 'Filename not included in file data.' );
+ }
+
+ return api.postWithEditToken( data ).then( function ( result ) {
+ if ( result.upload && ( result.upload.error || result.upload.warnings ) ) {
+ return $.Deferred().reject( result.upload.error || result.upload.warnings ).promise();
+ }
+ return result;
+ } );
+ }
+
+ return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) {
+ if ( result && result.upload && result.upload.filekey ) {
+ filekey = result.upload.filekey;
+ } else if ( result && ( result.error || result.warning ) ) {
+ return $.Deferred().reject( result );
+ }
+
+ return finishUpload;
+ } );
+ },
+
+ needToken: function () {
+ return true;
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.upload
+ */
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.watch
+ * @since 1.19
+ */
+( function ( mw, $ ) {
+
+ /**
+ * @private
+ * @static
+ * @context mw.Api
+ *
+ * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
+ * array thereof. If an array is passed, the return value passed to the promise will also be an
+ * array of appropriate objects.
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
+ * parameter)
+ * @return {string} return.done.watch.title Full pagename
+ * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
+ * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message
+ */
+ function doWatchInternal( pages, addParams ) {
+ // XXX: Parameter addParams is undocumented because we inherit this
+ // documentation in the public method...
+ var apiPromise = this.postWithToken( 'watch',
+ $.extend(
+ {
+ action: 'watch',
+ titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ),
+ uselang: mw.config.get( 'wgUserLanguage' )
+ },
+ addParams
+ )
+ );
+
+ return apiPromise
+ .then( function ( data ) {
+ // If a single page was given (not an array) respond with a single item as well.
+ return $.isArray( pages ) ? data.watch : data.watch[ 0 ];
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for `action=watch`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ watch: function ( pages ) {
+ return doWatchInternal.call( this, pages );
+ },
+
+ /**
+ * Convenience method for `action=watch&unwatch=1`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ unwatch: function ( pages ) {
+ return doWatchInternal.call( this, pages, { unwatch: 1 } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.watch
+ */
+
+}( mediaWiki, jQuery ) );